package IC.Semantic;

import java.util.*;
import IC.AST.*;
import IC.Tables.Symbols.*;
import IC.Tables.Types.*;

/**
 * Visitor for building all Symbol tables
 */
public class SymbolTableBuilder implements IC.AST.Visitor{
	private boolean hasMain = false;
	
	/**
	 * constructor
	 * @param icFileName
	 */
	public SymbolTableBuilder(String icFilePath){
		String[] path = icFilePath.split("\\\\");
		TypeTable.initTypeTable(path[path.length-1]);
	}
	
	/**
	 * @return IC file name
	 */
	public String getFileName(){
		return TypeTable.getFileName();
	}
	
	/**
	 * @param ms - A method symbol
	 * @return true if ms is the main method
	 */
	private static boolean isMainMethod(MethodSymbol ms){
		if (ms.getName().compareTo("main") != 0 || !ms.isStatic()) 
			return false;		
		MethodType mt = (MethodType) ms.getType();
		try{
			if (!mt.getReturnType().subtypeOf(TypeTable.getType("void"))) 
				return false;
			Iterator<IC.Tables.Types.Type> paramTypesIter = mt.getParamTypes().iterator();
			if (!paramTypesIter.hasNext()) 
				return false; 
			IC.Tables.Types.Type t = paramTypesIter.next();
			if (!t.subtypeOf(TypeTable.getArrayType(TypeTable.getType("string")))) 
				return false; 
			if (paramTypesIter.hasNext()) 
				return false; 
		}catch(SemanticError se) {
			System.err.println(se.toString());} 
		return true;
	}
	
	

	
	/**
	 * Creates a global symbol table and recursively updates it
	 * @return GlobalSymbolTable.  on error returns NULL
	 */
	public Object visit(Program program){
		
		GlobalSymbolTable global = new GlobalSymbolTable(TypeTable.getFileName());

		//Recursively add classes name to table 
		for (ICClass c: program.getClasses()){
			try{
				global.addClass(c);	
				c.setEnclosingScope(global);
			} catch (SemanticError se){
				se.setLine(c.getLine());
				System.err.println(se);
				return null; 
			}
		}
		
		// Recursively add classes data to table
		for (ICClass c: program.getClasses()){			
			ClassSymbolTable cst = (ClassSymbolTable) c.accept(this);
			if (cst == null) 
				return null; 
			else {
				if(c.hasSuperClass()){
					global.getClassSymbolTableRecursive(c.getSuperClassName()).addClassSymbolTable(cst);
				} else {
					global.addClassSymbolTable(cst);
				}
			}
		}
		
		// check if has main method
		if (!hasMain){
			System.err.println(new SemanticError("Program has no main method",0,""));
			return null;
		}
		return global;
	}
	
	

	
	/**
	 * Class table visitor
	 * (which conclude in creating the tables for each of the class members)
	 */
	public Object visit(ICClass icClass) {
		GlobalSymbolTable global = (GlobalSymbolTable) icClass.getEnclosingScope();
		ClassSymbolTable cst;		
		
		// if the class has a superclass its data should be added to it else it should has a new table
		if (icClass.hasSuperClass()) {
			cst = new ClassSymbolTable(icClass.getName(),
					global.getClassSymbolTableRecursive(icClass.getSuperClassName()),
					global);
		} else { 
			cst = new ClassSymbolTable(icClass.getName(),global);
		}
		
		
		for (Field f: icClass.getFields()){
			
			f.setEnclosingScope(cst);
			// check if previously defined
			try{
				cst.getFieldSymbolRecursive(f.getName());
				// if no exception then field is already defined
				System.err.println(new SemanticError("field is already defined",f.getLine(),f.getName()));
				return null;
			} catch (SemanticError unDefined){
				try{
					cst.getMethodSymbolRecursive(f.getName());
					// if no exception then field's name is already used for a method
					System.err.println(new SemanticError("Field's name already used",f.getLine(),f.getName()));
					return null;
				} catch (SemanticError unDefined2){ 
					try{
						cst.addFieldSymbol(f.getName(), f.getType().getFullName());
					} catch (SemanticError se){
						se.setLine(f.getLine());
						System.err.println(se);
						return null;
					}
				}
			}
			// recursively add Field's table
			if (f.accept(this) == null) 
				return null;
		}
		
		// methods:
		for (Method m: icClass.getMethods()){
			m.setEnclosingScope(cst);
			MethodSymbol ms;
			try{
				ms = new MethodSymbol(m);
				if (isMainMethod(ms)){
					if (hasMain){
						System.err.println(new SemanticError("Program already has main method",
								m.getLine(),
								ms.getName()));
						return null;
					} else {
						hasMain = true;
					}
				}
			} catch (SemanticError se){
				se.setLine(m.getLine());
				System.err.println(se);
				return null;
			}
			
			
			try{
				cst.getFieldSymbolRecursive(m.getName());
				System.err.println(new SemanticError("method's name is already used",m.getLine(),m.getName()));
				return null;
			} catch (SemanticError unDefined){ 
				try{
					cst.getMethodSymbol(m.getName());
					System.err.println(new SemanticError("method is already defined",m.getLine(),m.getName()));
					return null;
				} catch (SemanticError unDefined2){ 
					try{
						//Check if method is already defined in superclass and if override is allowed
						MethodSymbol superMethod = cst.getMethodSymbolRecursive(m.getName());
						if (!superMethod.getType().equals(ms.getType()) || (superMethod.isStatic() != ms.isStatic())){
							String str = "";
							if (ms.isStatic())
								str = "A static method cannot override a non-static method";
							else
								str = "A non-static method cannot override a static method";
							System.err.println(new SemanticError(str,m.getLine(),m.getName()));
							return null;
						} else { 
							cst.addMethodSymbol(m.getName(), ms);
						}
					}catch(SemanticError unDefined3){
						
						cst.addMethodSymbol(m.getName(), ms);
					}
				}
			}
		}
		
		// recursive method symbol table build
		for (Method m: icClass.getMethods()){
			MethodSymbolTable mst = (MethodSymbolTable) m.accept(this);
			if (mst == null) return null;
			else cst.addMethodSymbolTable(mst);
		}
		
		return cst;
	}


	
	/**
	 * Method visitor
	 */
	public MethodSymbolTable methodVisit(Method method){
		MethodSymbolTable mst = new MethodSymbolTable(method.getName(),
				(ClassSymbolTable)method.getEnclosingScope());
		
		method.getType().setEnclosingScope(mst);
		try{
			mst.setReturnVarSymbol(method.getType().getFullName());
		} catch (SemanticError se){
			se.setLine(method.getLine());
			System.err.println(se);
			return null;
		}
		
		for (Formal f: method.getFormals()){
			f.setEnclosingScope(mst);
			try{
				mst.getVarParamSymbol(f.getName());
				System.err.println(new SemanticError("parameter is previously defined in method "+method.getName(),
						f.getLine(),
						f.getName()));
				return null;
			} catch (SemanticError unDefined){ 
				try{
					mst.addParamSymbol(f.getName(), f.getType().getFullName());
				} catch (SemanticError se){
					se.setLine(f.getLine());
					System.err.println(se);
					return null;
				}
			}
			if (f.accept(this) == null) 
				return null;
		}
		
	
		for (Statement s: method.getStatements()){
			s.setEnclosingScope(mst);
			if (s.accept(this) == null) 
				return null; 
		}

		return mst;
	}
	
	
	/**
	 * static method visitor
	 */
	public Object visit(StaticMethod method) {
		MethodSymbolTable mst = methodVisit(method);
		if (mst == null) 
			return null;
		else 
			return mst;
	}
	
	
	/**
	 * virtual method visitor
	 */
	public Object visit(VirtualMethod method) {
		return methodVisit(method);
	}
	
	
	/**
	 * library method visitor
	 */
	public Object visit(LibraryMethod method) {
		return methodVisit(method);
	}
	
	
	
	/**
	 * StatementsBlock visitor
	 */
	public Object visit(StatementsBlock statementsBlock) {
		BlockSymbolTable bst = new BlockSymbolTable(statementsBlock.getEnclosingScope());
		BlockSymbolTable bst_base = (BlockSymbolTable) statementsBlock.getEnclosingScope();
		
		for (Statement s: statementsBlock.getStatements()){
			s.setEnclosingScope(bst);
			if (s.accept(this) == null) 
				return null;
		}
		
		bst_base.addBlockSymbolTable(bst);
		return bst;
	}
	

	

	/**
	 * LocalVariable visitor
	 */
	public Object visit(LocalVariable localVariable) {
		BlockSymbolTable bst = (BlockSymbolTable)localVariable.getEnclosingScope();
		try{
			bst.getVarSymbol(localVariable.getName());
			System.err.println(new SemanticError("variable is already defined",
					localVariable.getLine(),
					localVariable.getName()));
			return null;
		} catch (SemanticError unDefined){ 
			try{
				bst.addVarSymbol(localVariable.getName(), localVariable.getType().getFullName());
			} catch (SemanticError se){
				se.setLine(localVariable.getLine());
				System.err.println(se);
				return null;
			}
		}
		if (localVariable.hasInitValue()){
			localVariable.getInitValue().setEnclosingScope(localVariable.getEnclosingScope());
			if (localVariable.getInitValue().accept(this) == null) 
				return null;
		}
		
		localVariable.getType().setEnclosingScope(localVariable.getEnclosingScope());
		if (localVariable.getType().accept(this) == null) 
			return null;
		
		return true;
	}


	/**
	 * set enclosing scope and call the next visitor 
	 * @param node
	 */
	private Object visitorHelper (ASTNode current, SymbolTable enclosingScope) {
		current.setEnclosingScope(enclosingScope);
		if (current.accept(this)==null)
			return null;
		return true;		
	}
	
	/**
	 * Assignment visitor
	 */
	public Object visit(Assignment assignment) {
		if (visitorHelper(assignment.getVariable(), assignment.getEnclosingScope())==null)
			return null;
		if (visitorHelper(assignment.getAssignment(), assignment.getEnclosingScope())==null)
			return null;	
		return true;
	}
	
	
	/**
	 * If it's a break statement then return true (recursion stop condition)
	 */
	public Object visit(Break breakStatement) {
		return true;
	}	
	
	/**
	 * CallStatement visitor
	 */
	public Object visit(CallStatement callStatement) {
		if (visitorHelper(callStatement.getCall(), callStatement.getEnclosingScope())==null)
			return null;		
		return true;
	}

	
	/**
	 * If it's a continue statement then return true (recursion stop condition)
	 */
	public Object visit(Continue continueStatement) {
		return true;
	}
	
	
	/**
	 *If visitor
	 */
	public Object visit(If ifStatement) {
		if (visitorHelper(ifStatement.getCondition(), ifStatement.getEnclosingScope())==null)
			return null;		
		
		Statement operation = ifStatement.getOperation();
		if (operation instanceof LocalVariable){
			BlockSymbolTable bst = new BlockSymbolTable(ifStatement.getEnclosingScope());
			((BlockSymbolTable)ifStatement.getEnclosingScope()).addBlockSymbolTable(bst);
			operation.setEnclosingScope(bst);	
		} else operation.setEnclosingScope(ifStatement.getEnclosingScope());
		if (operation.accept(this) == null) 
			return null;
		
		if (ifStatement.hasElse()){
			
			Statement elseOperation = ifStatement.getElseOperation();
			if (elseOperation instanceof LocalVariable){
				BlockSymbolTable bst = new BlockSymbolTable(ifStatement.getEnclosingScope());
				((BlockSymbolTable)ifStatement.getEnclosingScope()).addBlockSymbolTable(bst);
				elseOperation.setEnclosingScope(bst);	
			} else elseOperation.setEnclosingScope(ifStatement.getEnclosingScope());
			if (elseOperation.accept(this) == null)
				return null;
		}
		
		return true;
	}

	
	
	/**
	 * Return visitor
	 */
	public Object visit(Return returnStatement) {
		if (returnStatement.hasValue()){
			if (visitorHelper(returnStatement.getValue(), returnStatement.getEnclosingScope())==null)
				return null;			
		}		
		return true;
	}

	
	
	/**
	 * While visitor
	 */
	public Object visit(While whileStatement) {
		if (visitorHelper(whileStatement.getCondition(), whileStatement.getEnclosingScope())==null)
			return null;
		
		
		Statement operation = whileStatement.getOperation();
		if (operation instanceof LocalVariable){
			BlockSymbolTable bst = new BlockSymbolTable(whileStatement.getEnclosingScope());
			((BlockSymbolTable)whileStatement.getEnclosingScope()).addBlockSymbolTable(bst);
			operation.setEnclosingScope(bst);	
		} else operation.setEnclosingScope(whileStatement.getEnclosingScope());
		if (operation.accept(this) == null)
			return null;
		
		return true;
	}
	
	
	
	/**
	 *ArrayLocation visitor
	 */
	public Object visit(ArrayLocation location) {
		if (visitorHelper(location.getArray(), location.getEnclosingScope())==null)
			return null;
		if (visitorHelper(location.getIndex(), location.getEnclosingScope())==null)
			return null;
		
		return true;
	}
	
	
	/**
	 *ExpressionBlock visitor
	 */
	public Object visit(ExpressionBlock expressionBlock) {
		if (visitorHelper(expressionBlock.getExpression(), expressionBlock.getEnclosingScope())==null)
			return null;		
		return true;
	}

	
	/**
	 * Field visitor
	 */
	public Object visit(Field field) {
		if (visitorHelper(field.getType(), field.getEnclosingScope())==null)
			return null;		
		return true;
	}

	
	/**
	 * Formal visitor
	 */
	public Object visit(Formal formal) {
		if (visitorHelper(formal.getType(), formal.getEnclosingScope())==null)
			return null;	
		return true;
	}

	
	/**
	 * Length visitor
	 */
	public Object visit(Length length) {
		if (visitorHelper(length.getArray(), length.getEnclosingScope())==null)
			return null;		
		return true;
	}

	
	/**
	 * If it's a Literal statement then return true (recursion stop condition)
	 */
	public Object visit(Literal literal) {
		return true;
	}

	/**
	 * BinaryOp visitor
	 */
	public Object binaryOpVisit(BinaryOp binaryOp){
		if (visitorHelper(binaryOp.getFirstOperand(), binaryOp.getEnclosingScope())==null)
			return null;	
		if (visitorHelper(binaryOp.getSecondOperand(), binaryOp.getEnclosingScope())==null)
			return null;		
		return true;
	}
	
	
	/**
	 * LogicalBinaryOp visitor
	 */
	public Object visit(LogicalBinaryOp binaryOp) {
		return binaryOpVisit(binaryOp);
	}
	
	
	/**
	 * MathBinaryOp visitor
	 */
	public Object visit(MathBinaryOp binaryOp) {
		return binaryOpVisit(binaryOp);
	}
	
	/**
	 * UnaryOp visit helper
	 */
	public Object unaryOpVisit(UnaryOp unaryOp){
		if (visitorHelper(unaryOp.getOperand(), unaryOp.getEnclosingScope())==null)
			return null;		
		return true;
	}

	
	/**
	 * LogicalUnaryOp visitor
	 */
	public Object visit(LogicalUnaryOp unaryOp) {
		return unaryOpVisit(unaryOp);
	}

	
	/**
	 * MathUnaryOp visitor
	 */
	public Object visit(MathUnaryOp unaryOp) {
		return unaryOpVisit(unaryOp);
	}

	
	/**
	 * NewArray visitor
	 */
	public Object visit(NewArray newArray) {
		if (visitorHelper(newArray.getType(), newArray.getEnclosingScope())==null)
			return null;		
		if (visitorHelper(newArray.getSize(), newArray.getEnclosingScope())==null)
			return null;
		
		return true;
	}

	
	/**
	 *  If it's a NewClass statement then return true (recursion stop condition)
	 */
	public Object visit(NewClass newClass) {
		return true;
	}

	
	/**
	 * If it's a PrimitiveType statement then return true (recursion stop condition)
	 */
	public Object visit(PrimitiveType type) {
		return true;
	}

	
	/**
	 * If it's a This statement then return true (recursion stop condition)
	 */
	public Object visit(This thisExpression) {
		return true;
	}

	
	/**
	 *If it's a UserType statement then return true (recursion stop condition)
	 */
	public Object visit(UserType type) {
		return true;
	}

	
	/**
	 * VariableLocation visitor
	 */
	public Object visit(VariableLocation location) {
		if (location.isExternal()){ 
			if (visitorHelper(location.getLocation(), location.getEnclosingScope())==null)
				return null;
		}else try{			
			((BlockSymbolTable) location.getEnclosingScope()).getVarSymbolRecursive(location.getName());
		} catch (SemanticError se){
			se.setLine(location.getLine());
			System.err.println(se);
			return null;
		}
		
		return true;
	}

	
	/**
	 * StaticCall visitor
	 */
	public Object visit(StaticCall call) {
		for (Expression e: call.getArguments()){
			if (visitorHelper(e, call.getEnclosingScope())==null)
				return null;			
		}
		
		return true;
	}
	
	
	/**
	 * VirtualCall visitor
	 */
	public Object visit(VirtualCall call) {
		if (call.isExternal()) {
			if (visitorHelper(call.getLocation(), call.getEnclosingScope())==null)
				return null;			
		}
		
		for (Expression e: call.getArguments()){
			if (visitorHelper(e, call.getEnclosingScope())==null)
				return null;			
		}
		
		return true;
	}

}
